Skip to content

fix(v3): prevent SIGSEGV enumerating screens on display change (macOS)#5516

Open
flofreud wants to merge 5 commits into
wailsapp:masterfrom
flofreud:fix/v3-macos-screen-params-crash
Open

fix(v3): prevent SIGSEGV enumerating screens on display change (macOS)#5516
flofreud wants to merge 5 commits into
wailsapp:masterfrom
flofreud:fix/v3-macos-screen-params-crash

Conversation

@flofreud
Copy link
Copy Markdown

@flofreud flofreud commented May 29, 2026

Description

On macOS, a v3 app crashes with a native SIGSEGV often when the display
configuration changes — sleep/wake, lid open/close, monitor connect/disconnect,
resolution or arrangement change.

After building with this fix the issue seems to be resolved.

Root cause

ApplicationDidChangeScreenParameters is dispatched on a background goroutine
(event_manager.go runs every application-event listener in its own go func),
and macOS emits the event several times in quick succession during a single
reconfiguration. The handler calls macosApp.processAndCacheScreens, which calls
[NSScreen screens] and messages each NSScreen. AppKit is main-thread-only, so
a burst of events runs getAllScreens concurrently, off the main thread,
while the display graph is itself being reconfigured → use-after-free / message to
a freed object → SIGSEGV. Because it is a native crash during cgo execution, the
dispatcher's defer handlePanic() cannot recover it and the process dies.

Observed at crash time: three goroutines simultaneously inside
processAndCacheScreens, each spawned by a separate handleApplicationEvent.

SIGSEGV: segmentation violation
signal arrived during cgo execution
_Cfunc_getAllScreens()
(*macosApp).processAndCacheScreens()      screen_darwin.go
(*macosApp).run.func3()                   application_darwin.go  // ApplicationDidChangeScreenParameters
(*EventManager).handleApplicationEvent.func1()

The per-screen [[NSScreen screens] firstObject] lookup added to processScreen
in alpha.91 (#5117) multiplied the off-main-thread AppKit access in this path and
made the crash much easier to hit.

Fix

  • Marshal screen enumeration onto the main thread via InvokeSync, guarding
    against a deadlock when already on the main thread. The main run loop also
    serialises the burst of events, so [NSScreen screens] is never accessed
    concurrently.
  • Resolve the primary-screen height once per refresh instead of
    re-enumerating [NSScreen screens] inside processScreen for every screen.
  • Return the screen count from getAllScreens so the Go side no longer reads
    the count separately via GetNumScreens — closing a TOCTOU where a display
    added between the two calls could make the loop over-read the malloc'd buffer.

How to test

  1. Run a v3 app on macOS with a window/system tray, ideally with an external monitor.
  2. Repeatedly connect/disconnect the monitor, or sleep/wake with a clamshell.
  3. Before: the app dies with the SIGSEGV above. After: screens re-cache cleanly.

go build ./pkg/application/ and go vet ./pkg/application/ pass.

Refs #5117

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a crash that occurred when display configuration changed (sleep/wake, monitor connect/disconnect).
    • Serialized screen-change handling to avoid concurrent access issues that could trigger crashes.
    • Ensured primary-screen height is resolved once per refresh for consistent coordinate handling.
    • Eliminated a race that could cause incorrect screen counts or sizing during display updates.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

Walkthrough

Refactors macOS screen handling to compute primary screen height once, change C getAllScreens to return a counted Screen* snapshot, and marshal AppKit screen enumeration onto the main thread to prevent SIGSEGV during display configuration changes.

Changes

Darwin Screen Enumeration SIGSEGV Fix

Layer / File(s) Summary
Primary screen height resolution
v3/pkg/application/screen_darwin.go
Adds primaryScreenHeight() and updates processScreen() to accept the resolved primary height so primary-screen frame height is computed once rather than per-screen.
Screen enumeration API and callsite updates
v3/pkg/application/screen_darwin.go
getAllScreens() now accepts an outCount pointer and returns a Screen* array sized from a single [NSScreen screens] snapshot; callers (GetPrimaryScreen, getScreenForWindow, getScreenForSystemTray) pass primaryScreenHeight() into processScreen().
Main thread enumeration marshalling
v3/pkg/application/screen_darwin.go
processAndCacheScreens() calls C.getAllScreens(&count), marshals and frees the returned C buffer, and ensures enumeration runs on the main thread (direct or via InvokeSync).
Changelog documentation
v3/UNRELEASED_CHANGELOG.md
Adds a Fixed entry summarizing the SIGSEGV crash fix, main-thread marshalling, once-per-refresh height resolution, and TOCTOU mitigation via returned count.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • wailsapp/wails#4818: Also adjusts macOS coordinate/dimension handling related to screen height and Y conversion.
  • wailsapp/wails#5304: Modifies Darwin Y-coordinate conversion and primary-screen baseline logic related to primaryScreenHeight().

Suggested labels

Bug, MacOS, awaiting review 🕝

Suggested reviewers

  • leaanthony
  • atterpac

Poem

🐰 I hopped through screens where shadows flop,
A SIGSEGV lurked when displays would swap.
Heights found once, counts snapped neat and tight,
Main thread keeps AppKit safe through day and night.
The rabbit cheers—no more crash in sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically summarizes the main fix: preventing a SIGSEGV when enumerating screens during display configuration changes on macOS.
Description check ✅ Passed The description is comprehensive and well-structured, including the root cause analysis, fix details, and testing instructions, though it does not explicitly fill the template sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@flofreud flofreud marked this pull request as ready for review May 29, 2026 18:24
`ApplicationDidChangeScreenParameters` is dispatched on background
goroutines (event_manager.go) and can fire several times in quick
succession during a single display reconfiguration. `macosApp.
processAndCacheScreens` enumerated `[NSScreen screens]` and messaged each
`NSScreen` off the main thread, so a burst of events ran `getAllScreens`
concurrently and crashed with a native SIGSEGV during cgo execution. As a
hard signal it is not catchable by the dispatcher's `defer handlePanic()`.

- Marshal the screen enumeration onto the main thread via `InvokeSync`,
  guarding against a deadlock when already on the main thread. Running on
  the main run loop also serialises the event burst so `[NSScreen screens]`
  is never touched concurrently.
- Resolve the primary-screen height once per refresh instead of
  re-enumerating `[NSScreen screens]` inside `processScreen` for every
  screen (introduced in alpha.91, which multiplied the off-main-thread
  AppKit access in this path).
- Return the screen count from `getAllScreens` so the Go side no longer
  reads the count separately via `GetNumScreens`, closing a TOCTOU that
  could over-read the malloc'd buffer when a display is added mid-refresh.

Refs wailsapp#5117

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@flofreud flofreud force-pushed the fix/v3-macos-screen-params-crash branch from 4b8327d to 7cad7ad Compare May 29, 2026 18:24
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
v3/pkg/application/screen_darwin.go (1)

113-126: ⚡ Quick win

Reuse the captured snapshot for primary height instead of re-enumerating.

getAllScreens snapshots [NSScreen screens] at Line 114 to size the allocation, but Line 119 calls primaryScreenHeight(), which enumerates [NSScreen screens] a second time. This both contradicts the "single snapshot" intent of the fix and means the primary height can come from a different snapshot than the count/loop. Since screens[0] is the primary screen (matching isPrimary = (i == 0)), derive the height from the already-captured array.

♻️ Derive primary height from the existing snapshot
 	if (outCount != NULL) {
 		*outCount = (int)count;
 	}
-	CGFloat primaryHeight = primaryScreenHeight();
+	// Reuse the snapshot above instead of re-enumerating [NSScreen screens];
+	// screens[0] is the primary screen (matches isPrimary = (i == 0) below).
+	CGFloat primaryHeight = count > 0 ? [[screens objectAtIndex:0] frame].size.height : 0;
 	Screen* returnScreens = malloc(sizeof(Screen) * count);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/screen_darwin.go` around lines 113 - 126, getAllScreens
currently snapshots NSArray *screens but then calls primaryScreenHeight(), which
re-enumerates NSScreen; instead derive the primary height from the
already-captured snapshot: after capturing screens (NSArray<NSScreen *>
*screens), compute primaryHeight using the first element ([screens
objectAtIndex:0])'s frame/visibleFrame height and pass that into processScreen
for all items so the primary height is consistent with the snapshot; update
getAllScreens to stop calling primaryScreenHeight() and use the height extracted
from screens[0] (keep the existing processScreen and isPrimary = (i == 0)
logic).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@v3/pkg/application/screen_darwin.go`:
- Around line 113-126: getAllScreens currently snapshots NSArray *screens but
then calls primaryScreenHeight(), which re-enumerates NSScreen; instead derive
the primary height from the already-captured snapshot: after capturing screens
(NSArray<NSScreen *> *screens), compute primaryHeight using the first element
([screens objectAtIndex:0])'s frame/visibleFrame height and pass that into
processScreen for all items so the primary height is consistent with the
snapshot; update getAllScreens to stop calling primaryScreenHeight() and use the
height extracted from screens[0] (keep the existing processScreen and isPrimary
= (i == 0) logic).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b7fc19a9-fd30-4730-9d6b-336c88e84d4f

📥 Commits

Reviewing files that changed from the base of the PR and between e34a2c4 and 7cad7ad.

📒 Files selected for processing (2)
  • v3/UNRELEASED_CHANGELOG.md
  • v3/pkg/application/screen_darwin.go

leaanthony and others added 2 commits May 30, 2026 15:08
getAllScreens snapshotted [NSScreen screens] to size the allocation but
then called primaryScreenHeight(), which enumerated [NSScreen screens] a
second time — contradicting the single-snapshot intent and risking a
primary height from a different snapshot than the count/loop. Derive it
from screens[0] (already treated as primary via isPrimary = i == 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
v3/pkg/application/screen_darwin.go (1)

261-272: 💤 Low value

Consider main-thread marshalling for remaining AppKit accessors.

getScreenForWindow and getScreenForSystray call C functions that access [NSScreen screens] (via primaryScreenHeight()) without main-thread marshalling. If invoked from a background goroutine during a display reconfiguration, they could hit the same SIGSEGV the PR fixes for processAndCacheScreens. These paths are lower risk since they're typically called in response to user actions rather than system events, but you may want to add similar protection in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/screen_darwin.go` around lines 261 - 272,
getScreenForWindow and getScreenForSystray call C functions that touch AppKit
([NSScreen screens]) and must be called on the main thread to avoid SIGSEGV;
wrap the calls to C.getScreenForWindow and C.getWindowForSystray in the same
main-thread marshalling used elsewhere (e.g., a runOnMain / runMainSync helper
or the pattern used in processAndCacheScreens) so the C.get* calls and
subsequent cScreenToScreen conversion run on the main thread, propagate any nil
cScreen as an error, and return the Screen only after the main-thread call
completes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@v3/pkg/application/screen_darwin.go`:
- Around line 261-272: getScreenForWindow and getScreenForSystray call C
functions that touch AppKit ([NSScreen screens]) and must be called on the main
thread to avoid SIGSEGV; wrap the calls to C.getScreenForWindow and
C.getWindowForSystray in the same main-thread marshalling used elsewhere (e.g.,
a runOnMain / runMainSync helper or the pattern used in processAndCacheScreens)
so the C.get* calls and subsequent cScreenToScreen conversion run on the main
thread, propagate any nil cScreen as an error, and return the Screen only after
the main-thread call completes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a2c54d8d-1a7b-4bd3-ba6b-cbc4204ea9f8

📥 Commits

Reviewing files that changed from the base of the PR and between 7cad7ad and fa5c77b.

📒 Files selected for processing (1)
  • v3/pkg/application/screen_darwin.go

@flofreud
Copy link
Copy Markdown
Author

@leaanthony I addressed the first CodeRabbit nit-pick but the one about Wails generally not considering main-thread marshalling observed by Claude and CodeRabbit should probably be a more focused effort later.

@leaanthony leaanthony enabled auto-merge (squash) May 31, 2026 12:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/UNRELEASED_CHANGELOG.md`:
- Line 27: Update the user-facing changelog text to use consistent US spelling:
replace “unmaximise” with “unmaximize” in the UNRELEASED_CHANGELOG.md entry that
reads "Fix minimum width/height constraints not enforced after window unmaximise
on Windows (`#4593`)"; ensure the rest of the line remains unchanged except for
that word so the entry reads "Fix minimum width/height constraints not enforced
after window unmaximize on Windows (`#4593`)".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5f76df91-e4e5-45ad-acd9-8051603076ec

📥 Commits

Reviewing files that changed from the base of the PR and between fa5c77b and c9769dc.

📒 Files selected for processing (1)
  • v3/UNRELEASED_CHANGELOG.md

## Fixed
<!-- Bug fixes -->
- Fix SIGSEGV when the display configuration changes (sleep/wake, monitor connect/disconnect). `ApplicationDidChangeScreenParameters` is dispatched on background goroutines and can fire repeatedly during a single reconfiguration, so `processAndCacheScreens` enumerated `[NSScreen screens]` concurrently off the main thread and crashed. Screen enumeration is now marshalled onto the main thread (which also serialises the event burst), the primary-screen height is resolved once per refresh instead of per screen, and `getAllScreens` returns its count to close a TOCTOU against the buffer size. (#5117)
- Fix minimum width/height constraints not enforced after window unmaximise on Windows (#4593)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use consistent spelling in user-facing changelog text.

Line 27 uses “unmaximise”; consider “unmaximize” for consistency with common US spelling in release notes.

🧰 Tools
🪛 LanguageTool

[grammar] ~27-~27: Ensure spelling is correct
Context: ...t constraints not enforced after window unmaximise on Windows (#4593) - Fix mouse click-th...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/UNRELEASED_CHANGELOG.md` at line 27, Update the user-facing changelog text
to use consistent US spelling: replace “unmaximise” with “unmaximize” in the
UNRELEASED_CHANGELOG.md entry that reads "Fix minimum width/height constraints
not enforced after window unmaximise on Windows (`#4593`)"; ensure the rest of the
line remains unchanged except for that word so the entry reads "Fix minimum
width/height constraints not enforced after window unmaximize on Windows
(`#4593`)".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants